/**
 * 
 */
package br.com.framework.apresentacao.bean;

import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.enterprise.context.Conversation;
import javax.inject.Inject;
import javax.persistence.PersistenceException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.Logger;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortOrder;

import br.com.framework.apresentacao.datamodel.LazyEntityDataModel;
import br.com.framework.negocio.api.BaseNegocio;
import br.com.framework.negocio.exception.NegocioException;
import br.com.framework.persistencia.api.BaseDao;
import br.com.framework.persistencia.model.BaseEntity;
import br.com.framework.persistencia.sort.Order;

/**
 * Bean base para um CRUD (Create, Retrieve, Update, Delete)
 * 
 * @author Cleber
 * 
 */
public abstract class CRUDBean<E extends BaseEntity, DAO extends BaseDao<E>, Negocio extends BaseNegocio<E, DAO>>
		extends BaseBean {

	public static final String PARAM_FACES_REDIRECT_TRUE = "?faces-redirect=true";

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	protected static final Logger LOGGER = Logger.getLogger(CRUDBean.class);

	@Inject
	private Conversation conversation;
	
	private Negocio negocio;

	private E entidade;

	private List<E> entidadeList = new ArrayList<E>();
	private LazyEntityDataModel<E> dataModel;
	private int pageSize = 5;
	
	private Class<E> entidadeClass;
	private String nomeEntidadeCamelCase;
	
	/**
	 * 
	 */
	public CRUDBean() {
		super();
	}

	/**
	 * @param entidadeClass
	 */
	public CRUDBean(Class<E> entidadeClass) {
		this.entidadeClass = entidadeClass;
	}
	
	/**
	 * @return the pageSize
	 */
	public int getPageSize() {
		return pageSize;
	}

	/**
	 * @param pageSize the pageSize to set
	 */
	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}
	
	public LazyDataModel<E> getDataModel() {
		return dataModel;
	}

	/**
	 * Carrega a lista de entidades. 
	 */
	@SuppressWarnings("serial")
	public void loadDataModel() {
		LOGGER.debug("Carregando lista de entidades");
		endConversation();
		initConversation();
		dataModel = new LazyEntityDataModel<E>(entidadeList) {
			@Override
			public List<E> load(int first, int pageSize, String sortField,
					SortOrder sortOrder, Map<String, String> filters) {
				entityList.clear();
				Order order = Order.valueOf(sortOrder.name());
	            try {
	            	setRowCount(getNegocio().getRecordCount(filters, true).intValue());
	            	entityList.addAll(getNegocio().findAll(first, pageSize, sortField, order, filters, true));
	            } catch (Exception e) {
	            	addMessageError("dataModel", "dataModel.erro.carregando", 
	            			getEntidadeClassName());
	                LOGGER.error("Falha carregando registros", e);
	            }
	            return entityList;
			}
		};
		
	    dataModel.setPageSize(pageSize);
	}

	/**
	 * @return
	 */
	public List<E> getEntidadeList() {
		return entidadeList;
	}

	/**
	 * @return the entidade
	 */
	public E getEntidade() {
		return entidade;
	}

	/**
	 * Indica se esta entidade já está persistida.
	 * @return
	 */
	public boolean isManaged() {
		return (getEntidade() != null) && (getEntidade().getId() != null);
	} 

	/**
	 * Persiste a entidade
	 * @return
	 */
	public String salvar() {
		if (conversation.isTransient()) {
			conversation.begin();
		}
		if (isManaged()) {
			LOGGER.debug("Salvando entidade existente: " + getEntidadeClassName());
			try {
				getNegocio().update(getEntidade());
			} catch (PersistenceException e) {
				addMessageError(null, getNomeEntidadeCamelCase() +".erro.persistencia.editar");
				LOGGER.error("Falha de persistência ao editar entidade " 
						+ getEntidadeClassName(), e);
			} catch (NegocioException e) {
				addMessageError(null, e.getMessage());
				LOGGER.error("Falha de negocio ao editar entidade " 
						+ getEntidadeClassName(), e);
			} catch (Exception e) {
				Throwable rootCause = ExceptionUtils.getRootCause(e);
				if (rootCause instanceof ConstraintViolationException) {
					Iterator<ConstraintViolation<?>> constraintIt = 
							((ConstraintViolationException)rootCause).getConstraintViolations().iterator();
					while (constraintIt.hasNext()) {
						ConstraintViolation<?> violation = constraintIt.next();
						addMessageError(violation.getPropertyPath().toString(), violation.getMessage());
					}
				} else {
					LOGGER.error("Erro ao editar entidade " 
							+ getEntidadeClassName(), e);
					addMessageError(null, rootCause.getMessage());
				}
				return null;
			}
		} else {
			LOGGER.debug("Salvando nova entidade: " + getEntidadeClassName());
			try {
				getNegocio().insert(getEntidade());
			} catch (PersistenceException e) {
				addMessageError(null, getNomeEntidadeCamelCase() +".erro.persistencia.inserir");
				LOGGER.error("Falha de persistência ao inserir entidade " 
						+ getEntidadeClassName(), e);
			} catch (NegocioException e) {
				addMessageError(null, e.getMessage());
				LOGGER.error("Falha de negocio ao inserir entidade " 
						+ getEntidadeClassName(), e);
			} catch (Exception e) {
				Throwable rootCause = ExceptionUtils.getRootCause(e);
				if (rootCause != null && rootCause instanceof ConstraintViolationException) {
					Iterator<ConstraintViolation<?>> constraintIt = 
							((ConstraintViolationException)rootCause).getConstraintViolations().iterator();
					while (constraintIt.hasNext()) {
						ConstraintViolation<?> violation = constraintIt.next();
						addMessageError(violation.getPropertyPath().toString(), violation.getMessage());
					}
				} else {
					LOGGER.error("Erro ao inserir entidade " 
							+ getEntidadeClassName(), e);
					addMessageError(null, rootCause.getMessage());
				}
				return null;
			}
		}
		conversation.end();
		
		return listar() + PARAM_FACES_REDIRECT_TRUE;
	}
	
	/**
	 * @return
	 */
	protected String getListingOutcome() {
		return getOutcomePrefix() + "List.xhtml";
	}
	
	/**
	 * @return
	 */
	protected String getEditingOutcome() {
		return getOutcomePrefix() + "Edit.xhtml";
	}
	
	/**
	 * @return
	 */
	protected String getOutcomePrefix() {
		return "/" + getNomeEntidadeCamelCase() + "/" + getNomeEntidadeCamelCase();
	}
	
	/**
	 * Retorna o nome da entidade em formato CamelCase. 
	 * Ex: 
	 * Nome da Classe: UsuarioExterno 
	 * Em CamelCase: usuarioExterno
	 * 
	 * @return
	 */
	private String getNomeEntidadeCamelCase() {
		if (nomeEntidadeCamelCase == null) {
			nomeEntidadeCamelCase = getEntidadeClassName();
			nomeEntidadeCamelCase = nomeEntidadeCamelCase.substring(0, 1).toLowerCase() 
					+ nomeEntidadeCamelCase.substring(1);
		}
		return nomeEntidadeCamelCase;
	}
	
	/**
	 * Cria uma nova instância da entidade.
	 * @return
	 */
	protected E criarEntidade() {
		LOGGER.debug("Criando entidade " + getEntidadeClassName());
		E entidade = null;
		try {
			entidade = getEntidadeClass().newInstance();
		} catch (Exception e) {
			addMessageError(null, getEntidadeClassName() + ".erro.criarInstancia");
			LOGGER.error("Falha ao instanciar entidade " + getEntidadeClassName(), e);
		}
		return entidade;
	}
	
	
	/**
	 * Lista as entidades existentes
	 * @return
	 */
	public String listar() {
		if (!conversation.isTransient()) {
			conversation.end();
		}
		loadDataModel();
		return getListingOutcome();
	}
	
	/**
	 * Cria uma nova entidade
	 * @return
	 */
	public String criar() {
		initConversation();
		
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Criando nova entidade: " + getEntidadeClassName());
		}
		entidade = criarEntidade();
		
		beforeOutcomeCreate();
		
		return getEditingOutcome();
	}
	
	/**
	 * Utilizado para inicializar a página de criação da entidade.
	 * Inicialização de coleções relacionadas devem ser realizadas
	 * em uma sobrecarga deste método.
	 */
	protected void beforeOutcomeCreate() {
		
	}
	
	/**
	 * Utilizado para inicializar a página de criação da entidade.
	 * Inicialização de coleções relacionadas devem ser realizadas
	 * em uma sobrecarga deste método.
	 */
	protected void beforeOutcomeEdit() {
		
	}

	/**
	 * Altera uma entidade existente
	 * @return
	 */
	public String editar() {
		initConversation();
		
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Carregando entidade para edição: " + getEntidadeClassName());
		}
		
		Long id = (Long) dataModel.getRowKey(dataModel.getRowData());
		entidade = getNegocio().findByID(id);
		
		beforeOutcomeEdit();
		return getEditingOutcome();
	}
	
	/**
	 * Remove uma entidade existente.
	 * @return
	 */
	public String remover() {
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Removendo entidade " + getEntidadeClassName());
		}
		initConversation();
		entidade = dataModel.getRowData();
		try {
			getNegocio().remove(entidade);
			LOGGER.debug("Entidade removida com sucesso. " + getEntidadeClassName());
		} catch (PersistenceException e) {
			addMessageError(null, getNomeEntidadeCamelCase() +".erro.persistencia.remover");
			LOGGER.error("Falha de persistência ao remover entidade/ID " 
					+ getEntidadeClassName() + "/" + entidade.getId(), e);
		} catch (NegocioException e) {
			addMessageError(null, e.getMessage());
			LOGGER.error("Falha de negocio ao remover entidade " 
					+ getEntidadeClassName() + "/" + entidade.getId(), e);
		}
		conversation.end();
		return listar() + PARAM_FACES_REDIRECT_TRUE;
	}

	/**
	 * Inicia uma conversação.
	 */
	public void initConversation() {
		if (conversation.isTransient()) {
			conversation.begin();
		}
	}
	
	/**
	 * Finaliza uma conversação.
	 */
	public void endConversation() {
		if (!conversation.isTransient()) {
			conversation.end();
		}
	}

	/**
	 * Cancela uma operação
	 * @return
	 */
	public String cancelar() {
		if (!conversation.isTransient()) {
			conversation.end();
		}
		return listar() + PARAM_FACES_REDIRECT_TRUE;
	}

	/**
	 * Retorna a classe da entidade
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public Class<E> getEntidadeClass() {
		if (entidadeClass == null) {
			ParameterizedType parameterizedType = (ParameterizedType) getClass()
					.getGenericSuperclass();
			entidadeClass =  (Class<E>) parameterizedType.getActualTypeArguments()[0];
		}
		return entidadeClass;
	}
	
	/**
	 * Retorna o simpleName da classe da entidade.
	 * @return
	 */
	public String getEntidadeClassName() {
		return getEntidadeClass().getSimpleName();
	}
	
	/**
	 * @param entidade
	 *            the entidade to set
	 */
	public void setEntidade(E entidade) {
		this.entidade = entidade;
	}

	/**
	 * @return the negocio
	 */
	protected Negocio getNegocio() {
		return negocio;
	}

	/**
	 * @param negocio the negocio to set
	 */
	protected void setNegocio(Negocio negocio) {
		this.negocio = negocio;
	}

}
